package com.progenies.ecg.asm31.model.codeparts.lines;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.progenies.ecg.asm31.util.BytecodeInstructionsUtils;
import com.progenies.ecg.asm31.util.GenerationUtils;
import com.progenies.ecg.asm31.util.VariableRegister.Variable;
import com.progenies.ecg.model.ValueReference;



/**
 * Sets a static value to a variable, either local or field class. Their scope is obtained from the variable name automatically.
 * 
 * A static value is a "hardcoded" one, not dynamically generated.
 * 
 * No type checking is performed over the value at compiling time.
 *  
 * @author ofc587a87
 *
 */
public class SetVariableValueLinePart<T> extends ConfigurableLineCodePart
{
	public static final String FORZED_NULL = "_^*_FORZED_{%$$%}_NULL_*^_";

	/**
	 * Name of the variable that the value will be assigned to. 
	 */
	private String variableName;
	
	private String originValueVariable;
	
	/**
	 * Value to assign to the variable. There are no checking at compiling time of type association.
	 */
	private T value;

	private String fieldInsideOrigin;

	private Class<?> fieldInsideOriginType;

	private Class<?> originOwnerClass;
	
	/** By default, types will be converted automatically */
	private boolean automaticConversion=true;

	private Class<?> targetOwnerClass;
	private String targetOwnerVariable;
	
	public SetVariableValueLinePart(String varName)
	{
		this.variableName=varName;
	}
	
	


	@Override
	public void generateBytecode() {
		if(this.visitor==null )
			throw new IllegalStateException("LinePart has not been configurated, please use ClassGenerator");
		
		
		
		//if it's an external value in an external class, set it
		if(this.targetOwnerClass!=null)
		{
			//it accepts null values
			BytecodeInstructionsUtils.insertValueReferenceIntoStack(visitor, ValueReference.forStaticValue(value), varRegister, parent);
			
			Type targetType=null;
			try {
				targetType = Type.getType(targetOwnerClass.getField(variableName).getType());
			} catch (Exception e) {
				throw new RuntimeException("Field not found", e);
			}

			if(this.value!=null)
			{
				Type originType=Type.getType(this.value.getClass());
				BytecodeInstructionsUtils.checkAndConvertValue(visitor, originType, targetType, automaticConversion);
			}
			
			visitor.visitFieldInsn(Opcodes.PUTSTATIC, Type.getInternalName(this.targetOwnerClass), this.variableName, targetType.getDescriptor());
		}
		else if(this.targetOwnerVariable!=null)
		{
			
			Variable targetField=varRegister.getVariable(this.targetOwnerVariable);
			Field fieldObj=null;
			try {
				fieldObj=Class.forName(targetField.getType().getClassName()).getField(variableName);;
			} catch (Exception e) {
				throw new RuntimeException("Field not found", e);
			}
			Type targetType=Type.getType(fieldObj.getType());

			
			
			//if it's not an static field, insert reference to instance object
			boolean isStatic=(Modifier.isStatic(fieldObj.getModifiers()));
			if(!isStatic)
			{
				BytecodeInstructionsUtils.insertLocalVariableIntoStack(visitor, varRegister, "this");
				visitor.visitFieldInsn(Opcodes.GETFIELD, GenerationUtils.getFieldOwner(this.targetOwnerVariable, parent), this.targetOwnerVariable, targetField.getType().getDescriptor());			
			}
			
			//value to insert, it accepts null values
			BytecodeInstructionsUtils.insertValueReferenceIntoStack(visitor, ValueReference.forStaticValue(value), varRegister, parent);
			
			if(this.value!=null)
			{
				Type originType=Type.getType(this.value.getClass());
				BytecodeInstructionsUtils.checkAndConvertValue(visitor, originType, targetType, automaticConversion);
			}
			

			visitor.visitFieldInsn(isStatic?Opcodes.PUTSTATIC:Opcodes.PUTFIELD, targetField.getType().getInternalName(), this.variableName, targetType.getDescriptor());
						
		}
		else
		{
			Type originType=null;
			
			//is a local variable?
			//TODO: Variable not found -> NullPointer???
			Variable var=varRegister.getVariable(this.variableName);
			if(!var.isLocal())
			{
				//if it's not local, i'll need to push "this" into stack
				BytecodeInstructionsUtils.insertLocalVariableIntoStack(this.visitor, varRegister, "this");
			}
			
			//if i need to insert an static value, insert into stack
			if(this.originValueVariable==null && this.value!=null)
			{
				if(value.equals(FORZED_NULL))
				{
					
					originType=var.getType();
					
					//can't assign null to a primitive field/variable
					if(originType.getSort()!=Type.OBJECT && originType.getSort()!=Type.ARRAY)
						throw new RuntimeException("The variable type is primitive or array, can't accept null values");
					
					visitor.visitInsn(Opcodes.ACONST_NULL);
				}
				else
				{
					originType=Type.getType(value.getClass());
					BytecodeInstructionsUtils.insertValueIntoStack(visitor, value);
					
				}
			}
			else if(this.originValueVariable!=null) //i need to read a value from a variable
			{
				Variable originVar=varRegister.getVariable(this.originValueVariable);
				originType=originVar.getType();
				
				if(originVar.isLocal()) //if it's local, just insert into stack
					visitor.visitVarInsn(originVar.getType().getOpcode(Opcodes.ILOAD), originVar.getIndex());
				else
				{ 	//i need to search for another local field. If var is local, i have "this" on stack, so duplicate it to invoke a GETFIELD on it
					if(!var.isLocal())
						visitor.visitInsn(Opcodes.DUP);
					else //if local, i'll need to push "this"
					{
						//if it's not local, i'll need to push "this" into stack
						BytecodeInstructionsUtils.insertLocalVariableIntoStack(this.visitor, varRegister, "this");
					}
					
					visitor.visitFieldInsn((originVar.isStatic()?Opcodes.GETSTATIC:Opcodes.GETFIELD), GenerationUtils.getFieldOwner(originVar.getName(), parent), originVar.getName(), originVar.getType().getDescriptor());
					//after GETxxxxxx, the stacks remains with the first "this" if not local, and the field value
				}
				
				//if the variable was an object that holds the value in a field, i'll get that field
				if(this.fieldInsideOrigin!=null) //instance field, NOT static
				{
					visitor.visitFieldInsn(Opcodes.GETFIELD, originVar.getType().getInternalName(), this.fieldInsideOrigin, Type.getDescriptor(fieldInsideOriginType));
					originType=Type.getType(fieldInsideOriginType);
				}
			}
			else if(this.originOwnerClass!=null && this.fieldInsideOrigin!=null)
			{
				//i'll read the value from the static field
				visitor.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(originOwnerClass), this.fieldInsideOrigin, Type.getDescriptor(fieldInsideOriginType));
				originType=Type.getType(fieldInsideOriginType);
			}
			
			//now, we will check if conversion is needed
			if(originType!=null)
			{
				BytecodeInstructionsUtils.checkAndConvertValue(visitor, originType, var.getType(), automaticConversion);
			}
			
			
			
			//depending the variable type, the store method will be different
			if(var.isLocal()) //the stack will have just the new value
			{
				//just store it
				visitor.visitVarInsn(var.getType().getOpcode(Opcodes.ISTORE), var.getIndex());
			}
			else //the stack have "this" and the new value
			{
				visitor.visitFieldInsn((var.isStatic()?Opcodes.PUTSTATIC:Opcodes.PUTFIELD), GenerationUtils.getFieldOwner(var.getName(), parent), var.getName(), var.getType().getDescriptor());
			}
		}
	}




	public String getVariableName() {
		return variableName;
	}

	public void setVariableName(String variableName) {
		this.variableName = variableName;
	}

	public T getValue() {
		return value;
	}

	@SuppressWarnings("unchecked")
	public void setValue(T value) {
		this.value = (T) (value==null?FORZED_NULL:value);
	}






	public String getOriginValueVariable() {
		return originValueVariable;
	}



	public void setOriginValueVariable(String originValueVariable) {
		this.originValueVariable = originValueVariable;
	}




	public void setFieldInsideOrigin(String fieldName) {
		this.fieldInsideOrigin=fieldName;
	}
	
	public String getFieldInsideOrigin()
	{
		return this.fieldInsideOrigin;
	}




	public void setFieldInsideOriginType(Class<?> fieldType) {
		this.fieldInsideOriginType=fieldType;
	}
	
	public Class<?> getFieldInsideOriginType()
	{
		return this.fieldInsideOriginType;
	}




	public void setOriginOwnerClass(Class<?> ownerClass) {
		this.originOwnerClass=ownerClass;
	}
	
	public Class<?> getOriginOwnerClass()
	{
		return this.originOwnerClass;
	}




	public boolean isAutomaticConversion() {
		return automaticConversion;
	}




	public void setAutomaticConversion(boolean automaticConversion) {
		this.automaticConversion = automaticConversion;
	}




	public void setTargetOwnerClass(Class<?> ownerClass)
	{
		this.targetOwnerClass=ownerClass;
	}




	public String getTargetOwnerVariable() {
		return targetOwnerVariable;
	}




	public void setTargetOwnerVariable(String targetOwnerVariable) {
		this.targetOwnerVariable = targetOwnerVariable;
	}

}
